package com.knowledgegraph.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.knowledgegraph.dto.G6GraphData;
import com.knowledgegraph.entity.Node;
import com.knowledgegraph.entity.NodeRelation;
import com.knowledgegraph.repository.NodeRelationRepository;
import com.knowledgegraph.repository.NodeRepository;
import com.knowledgegraph.repository.ResourceRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
public class G6DataConversionService {
    
    @Autowired
    private NodeRepository nodeRepository;
    
    @Autowired
    private NodeRelationRepository nodeRelationRepository;
    
    @Autowired
    private ResourceRepository resourceRepository;
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    public G6GraphData convertToG6Format() {
        List<Node> nodes = nodeRepository.findAll();
        List<NodeRelation> relations = nodeRelationRepository.findAll();
        
        List<G6GraphData.G6Node> g6Nodes = nodes.stream()
            .map(this::convertNodeToG6Node)
            .collect(Collectors.toList());
            
        List<G6GraphData.G6Edge> g6Edges = relations.stream()
            .map(this::convertRelationToG6Edge)
            .collect(Collectors.toList());
            
        List<G6GraphData.G6Combo> g6Combos = generateCombosByNodeType(nodes);
        
        return G6GraphData.builder()
            .nodes(g6Nodes)
            .edges(g6Edges)
            .combos(g6Combos)
            .build();
    }
    
    public G6GraphData convertSubGraphToG6Format(List<Long> nodeIds) {
        List<Node> nodes = nodeRepository.findAllById(nodeIds);
        List<NodeRelation> relations = nodeRelationRepository.findRelationsForNodes(nodeIds);
        
        List<G6GraphData.G6Node> g6Nodes = nodes.stream()
            .map(this::convertNodeToG6Node)
            .collect(Collectors.toList());
            
        List<G6GraphData.G6Edge> g6Edges = relations.stream()
            .map(this::convertRelationToG6Edge)
            .collect(Collectors.toList());
            
        List<G6GraphData.G6Combo> g6Combos = generateCombosByNodeType(nodes);
        
        return G6GraphData.builder()
            .nodes(g6Nodes)
            .edges(g6Edges)
            .combos(g6Combos)
            .build();
    }
    
    public G6GraphData convertTreeStructureToG6Format(Long rootNodeId, int maxDepth) {
        Set<Long> visitedNodes = new HashSet<>();
        Set<Long> visitedEdges = new HashSet<>();
        
        List<G6GraphData.G6Node> g6Nodes = new ArrayList<>();
        List<G6GraphData.G6Edge> g6Edges = new ArrayList<>();
        
        buildTreeRecursive(rootNodeId, 0, maxDepth, visitedNodes, visitedEdges, g6Nodes, g6Edges);
        
        List<Node> allNodes = nodeRepository.findAllById(visitedNodes);
        List<G6GraphData.G6Combo> g6Combos = generateCombosByNodeType(allNodes);
        
        return G6GraphData.builder()
            .nodes(g6Nodes)
            .edges(g6Edges)
            .combos(g6Combos)
            .build();
    }
    
    private void buildTreeRecursive(Long nodeId, int currentDepth, int maxDepth, 
                                  Set<Long> visitedNodes, Set<Long> visitedEdges,
                                  List<G6GraphData.G6Node> g6Nodes, List<G6GraphData.G6Edge> g6Edges) {
        
        if (currentDepth > maxDepth || visitedNodes.contains(nodeId)) {
            return;
        }
        
        Optional<Node> nodeOpt = nodeRepository.findById(nodeId);
        if (!nodeOpt.isPresent()) {
            return;
        }
        
        Node node = nodeOpt.get();
        visitedNodes.add(nodeId);
        g6Nodes.add(convertNodeToG6Node(node));
        
        List<NodeRelation> outgoingRelations = nodeRelationRepository.findBySourceNodeId(nodeId);
        
        for (NodeRelation relation : outgoingRelations) {
            if (!visitedEdges.contains(relation.getId())) {
                visitedEdges.add(relation.getId());
                g6Edges.add(convertRelationToG6Edge(relation));
                
                buildTreeRecursive(relation.getTargetNode().getId(), currentDepth + 1, 
                                 maxDepth, visitedNodes, visitedEdges, g6Nodes, g6Edges);
            }
        }
    }
    
    private G6GraphData.G6Node convertNodeToG6Node(Node node) {
        Map<String, Object> style = parseJsonToMap(node.getStyleConfig());
        Map<String, Object> properties = parseJsonToMap(node.getProperties());
        
        Map<String, Object> data = new HashMap<>();
        data.put("description", node.getDescription());
        data.put("nodeType", node.getNodeType());
        data.put("createdAt", node.getCreatedAt());
        data.put("updatedAt", node.getUpdatedAt());
        data.putAll(properties);
        
        return G6GraphData.G6Node.builder()
            .id(String.valueOf(node.getId()))
            .label(node.getName())
            .type(getNodeTypeForG6(node.getNodeType()))
            .x(node.getXPosition())
            .y(node.getYPosition())
            .style(style)
            .data(data)
            .comboId(node.getNodeType())
            .build();
    }
    
    private G6GraphData.G6Edge convertRelationToG6Edge(NodeRelation relation) {
        Map<String, Object> style = new HashMap<>();
        Map<String, Object> properties = parseJsonToMap(relation.getProperties());
        
        if (relation.getWeight() != null) {
            double strokeWidth = Math.max(1, Math.min(10, relation.getWeight() * 5));
            style.put("stroke-width", strokeWidth);
        }
        
        Map<String, Object> data = new HashMap<>();
        data.put("description", relation.getDescription());
        data.put("weight", relation.getWeight());
        data.put("isDirected", relation.getIsDirected());
        data.put("createdAt", relation.getCreatedAt());
        data.put("updatedAt", relation.getUpdatedAt());
        data.putAll(properties);
        
        return G6GraphData.G6Edge.builder()
            .id(String.valueOf(relation.getId()))
            .source(String.valueOf(relation.getSourceNode().getId()))
            .target(String.valueOf(relation.getTargetNode().getId()))
            .label(relation.getRelationType())
            .type(getEdgeTypeForG6(relation.getRelationType(), relation.getIsDirected()))
            .weight(relation.getWeight())
            .style(style)
            .data(data)
            .build();
    }
    
    private List<G6GraphData.G6Combo> generateCombosByNodeType(List<Node> nodes) {
        Map<String, List<Node>> nodesByType = nodes.stream()
            .filter(node -> node.getNodeType() != null)
            .collect(Collectors.groupingBy(Node::getNodeType));
            
        return nodesByType.entrySet().stream()
            .map(entry -> {
                String nodeType = entry.getKey();
                List<Node> typeNodes = entry.getValue();
                
                Map<String, Object> style = getComboStyleByType(nodeType);
                Map<String, Object> data = new HashMap<>();
                data.put("nodeCount", typeNodes.size());
                data.put("nodeType", nodeType);
                
                return G6GraphData.G6Combo.builder()
                    .id(nodeType)
                    .label(nodeType + " (" + typeNodes.size() + ")")
                    .type("rect")
                    .style(style)
                    .data(data)
                    .build();
            })
            .collect(Collectors.toList());
    }
    
    private Map<String, Object> parseJsonToMap(String json) {
        if (json == null || json.trim().isEmpty()) {
            return new HashMap<>();
        }
        
        try {
            return objectMapper.readValue(json, Map.class);
        } catch (JsonProcessingException e) {
            log.warn("Failed to parse JSON: {}", json, e);
            return new HashMap<>();
        }
    }
    
    private String getNodeTypeForG6(String nodeType) {
        if (nodeType == null) {
            return "circle";
        }
        
        switch (nodeType.toLowerCase()) {
            case "person":
            case "user":
                return "image";
            case "document":
            case "file":
                return "rect";
            case "concept":
            case "topic":
                return "ellipse";
            default:
                return "circle";
        }
    }
    
    private String getEdgeTypeForG6(String relationType, Boolean isDirected) {
        if (isDirected == null || !isDirected) {
            return "line";
        }
        
        if (relationType != null) {
            switch (relationType.toLowerCase()) {
                case "hierarchy":
                case "parent":
                case "child":
                    return "polyline";
                case "reference":
                case "link":
                    return "quadratic";
                default:
                    return "line";
            }
        }
        
        return "line";
    }
    
    private Map<String, Object> getComboStyleByType(String nodeType) {
        Map<String, Object> style = new HashMap<>();
        
        if (nodeType != null) {
            switch (nodeType.toLowerCase()) {
                case "person":
                case "user":
                    style.put("fill", "#e6f7ff");
                    style.put("stroke", "#1890ff");
                    break;
                case "document":
                case "file":
                    style.put("fill", "#f6ffed");
                    style.put("stroke", "#52c41a");
                    break;
                case "concept":
                case "topic":
                    style.put("fill", "#fff2e8");
                    style.put("stroke", "#fa8c16");
                    break;
                default:
                    style.put("fill", "#f0f0f0");
                    style.put("stroke", "#d9d9d9");
                    break;
            }
        }
        
        style.put("stroke-width", 2);
        style.put("radius", 5);
        
        return style;
    }
}